 /*******************************************************************************
  * Copyright (c) 2005, 2006 IBM Corporation and others.
  * All rights reserved. This program and the accompanying materials
  * are made available under the terms of the Eclipse Public License v1.0
  * which accompanies this distribution, and is available at
  * http://www.eclipse.org/legal/epl-v10.html
  *
  * Contributors:
  * IBM Corporation - initial API and implementation
  *******************************************************************************/

 package org.eclipse.ui.internal.preferences;

 import java.util.Arrays ;
 import java.util.HashMap ;
 import java.util.HashSet ;
 import java.util.Iterator ;
 import java.util.Map ;

 import org.eclipse.core.commands.common.EventManager;
 import org.eclipse.core.runtime.preferences.IEclipsePreferences;
 import org.eclipse.core.runtime.preferences.IPreferenceNodeVisitor;
 import org.osgi.service.prefs.BackingStoreException;
 import org.osgi.service.prefs.Preferences;

 /**
  * Represents a working copy of a preference node, backed by the real node.
  * <p>
  * Note: Working copy nodes do not fire node change events.
  * </p>
  * <p>
  * Note: Preference change listeners registered on this node will only receive
  * events from this node and not events based on the original backing node.
  * </p>
  * @since 3.1
  */
 public class WorkingCopyPreferences extends EventManager implements
         IEclipsePreferences {

     private static final String TRUE = "true"; //$NON-NLS-1$

     private final Map temporarySettings;
     private final IEclipsePreferences original;
     private boolean removed = false;
     private org.eclipse.ui.preferences.WorkingCopyManager manager;

     /**
      * @param original the underlying preference node
      * @param manager the working copy manager
      */
     public WorkingCopyPreferences(IEclipsePreferences original, org.eclipse.ui.preferences.WorkingCopyManager manager) {
         super();
         this.original = original;
         this.manager = manager;
         this.temporarySettings = new HashMap ();
     }

     /*
      * Convenience method for throwing an exception when methods
      * are called on a removed node.
      */
     private void checkRemoved() {
         if (removed) {
             String message = "Preference node: " + absolutePath() + " has been removed."; //$NON-NLS-1$ //$NON-NLS-2$
 throw new IllegalStateException (message);
         }
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.preferences.IEclipsePreferences#addNodeChangeListener(org.eclipse.core.runtime.preferences.IEclipsePreferences.INodeChangeListener)
      */
     public void addNodeChangeListener(INodeChangeListener listener) {
         // no-op - working copy nodes don't fire node change events
 }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.preferences.IEclipsePreferences#removeNodeChangeListener(org.eclipse.core.runtime.preferences.IEclipsePreferences.INodeChangeListener)
      */
     public void removeNodeChangeListener(INodeChangeListener listener) {
         // no-op - working copy nodes don't fire node change events
 }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.preferences.IEclipsePreferences#addPreferenceChangeListener(org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener)
      */
     public void addPreferenceChangeListener(IPreferenceChangeListener listener) {
         checkRemoved();
         addListenerObject(listener);
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.preferences.IEclipsePreferences#removePreferenceChangeListener(org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener)
      */
     public void removePreferenceChangeListener(IPreferenceChangeListener listener) {
         checkRemoved();
         removeListenerObject(listener);
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#removeNode()
      */
     public void removeNode() throws BackingStoreException {
         checkRemoved();

         // clear all values (long way so people get notified)
 String [] keys = keys();
         for (int i = 0; i < keys.length; i++) {
             remove(keys[i]);
         }

         // remove children
 String [] childNames = childrenNames();
         for (int i = 0; i < childNames.length; i++) {
             node(childNames[i]).removeNode();
         }

         // mark as removed
 removed = true;
     }


     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#node(java.lang.String)
      */
     public Preferences node(String path) {
         checkRemoved();
         return manager.getWorkingCopy((IEclipsePreferences) getOriginal().node(path));
     }

     /* (non-Javadoc)
      * @see org.eclipse.core.runtime.preferences.IEclipsePreferences#accept(org.eclipse.core.runtime.preferences.IPreferenceNodeVisitor)
      */
     public void accept(IPreferenceNodeVisitor visitor) throws BackingStoreException {
         checkRemoved();
         if (!visitor.visit(this)) {
             return;
         }
         String [] childNames = childrenNames();
         for (int i = 0; i < childNames.length; i++) {
             ((IEclipsePreferences) node(childNames[i])).accept(visitor);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#put(java.lang.String, java.lang.String)
      */
     public void put(String key, String value) {
         checkRemoved();
         if (key == null || value == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         temporarySettings.put(key, value);
         if (!value.equals(oldValue)) {
             firePropertyChangeEvent(key, oldValue, value);
         }
     }

     private void firePropertyChangeEvent(String key, Object oldValue, Object newValue) {
         Object [] listeners = getListeners();
         if (listeners.length == 0) {
             return;
         }
         PreferenceChangeEvent event = new PreferenceChangeEvent(this, key, oldValue, newValue);
         for (int i = 0; i < listeners.length; i++) {
             ((IPreferenceChangeListener) listeners[i]).preferenceChange(event);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#get(java.lang.String, java.lang.String)
      */
     public String get(String key, String defaultValue) {
         checkRemoved();
         return internalGet(key, defaultValue);
     }

     private String internalGet(String key, String defaultValue) {
         if (key == null) {
             throw new NullPointerException ();
         }
         if (temporarySettings.containsKey(key)) {
             Object value = temporarySettings.get(key);
             return value == null ? defaultValue : (String ) value;
         }
         return getOriginal().get(key, defaultValue);
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#remove(java.lang.String)
      */
     public void remove(String key) {
         checkRemoved();
         if (key == null) {
             throw new NullPointerException ();
         }
         Object oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = temporarySettings.get(key);
         } else {
             oldValue = original.get(key, null);
         }
         if (oldValue == null) {
             return;
         }
         temporarySettings.put(key, null);
         firePropertyChangeEvent(key, oldValue, null);
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#clear()
      */
     public void clear() {
         checkRemoved();
         for (Iterator i = temporarySettings.keySet().iterator(); i.hasNext();) {
             String key = (String ) i.next();
             Object value = temporarySettings.get(key);
             if (value != null) {
                 temporarySettings.put(key, null);
                 firePropertyChangeEvent(key, value, null);
             }
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#putInt(java.lang.String, int)
      */
     public void putInt(String key, int value) {
         checkRemoved();
         if (key == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         String newValue = Integer.toString(value);
         temporarySettings.put(key, newValue);
         if (!newValue.equals(oldValue)) {
             firePropertyChangeEvent(key, oldValue, newValue);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#getInt(java.lang.String, int)
      */
     public int getInt(String key, int defaultValue) {
         checkRemoved();
         String value = internalGet(key, null);
         int result = defaultValue;
         if (value != null) {
             try {
                 result = Integer.parseInt(value);
             } catch (NumberFormatException e) {
                 // use default
 }
         }
         return result;
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#putLong(java.lang.String, long)
      */
     public void putLong(String key, long value) {
         checkRemoved();
         if (key == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         String newValue = Long.toString(value);
         temporarySettings.put(key, newValue);
         if (!newValue.equals(oldValue)) {
             firePropertyChangeEvent(key, oldValue, newValue);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#getLong(java.lang.String, long)
      */
     public long getLong(String key, long defaultValue) {
         checkRemoved();
         String value = internalGet(key, null);
         long result = defaultValue;
         if (value != null) {
             try {
                 result = Long.parseLong(value);
             } catch (NumberFormatException e) {
                 // use default
 }
         }
         return result;
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#putBoolean(java.lang.String, boolean)
      */
     public void putBoolean(String key, boolean value) {
         checkRemoved();
         if (key == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         String newValue = String.valueOf(value);
         temporarySettings.put(key, newValue);
         if (!newValue.equalsIgnoreCase(oldValue)) {
             firePropertyChangeEvent(key, oldValue, newValue);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#getBoolean(java.lang.String, boolean)
      */
     public boolean getBoolean(String key, boolean defaultValue) {
         checkRemoved();
         String value = internalGet(key, null);
         return value == null ? defaultValue : TRUE.equalsIgnoreCase(value);
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#putFloat(java.lang.String, float)
      */
     public void putFloat(String key, float value) {
         checkRemoved();
         if (key == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         String newValue = Float.toString(value);
         temporarySettings.put(key, newValue);
         if (!newValue.equals(oldValue)) {
             firePropertyChangeEvent(key, oldValue, newValue);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#getFloat(java.lang.String, float)
      */
     public float getFloat(String key, float defaultValue) {
         checkRemoved();
         String value = internalGet(key, null);
         float result = defaultValue;
         if (value != null) {
             try {
                 result = Float.parseFloat(value);
             } catch (NumberFormatException e) {
                 // use default
 }
         }
         return result;
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#putDouble(java.lang.String, double)
      */
     public void putDouble(String key, double value) {
         checkRemoved();
         if (key == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         String newValue = Double.toString(value);
         temporarySettings.put(key, newValue);
         if (!newValue.equals(oldValue)) {
             firePropertyChangeEvent(key, oldValue, newValue);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#getDouble(java.lang.String, double)
      */
     public double getDouble(String key, double defaultValue) {
         checkRemoved();
         String value = internalGet(key, null);
         double result = defaultValue;
         if (value != null) {
             try {
                 result = Double.parseDouble(value);
             } catch (NumberFormatException e) {
                 // use default
 }
         }
         return result;
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#putByteArray(java.lang.String, byte[])
      */
     public void putByteArray(String key, byte[] value) {
         checkRemoved();
         if (key == null || value == null) {
             throw new NullPointerException ();
         }
         String oldValue = null;
         if (temporarySettings.containsKey(key)) {
             oldValue = (String ) temporarySettings.get(key);
         } else {
             oldValue = getOriginal().get(key, null);
         }
         String newValue = new String (Base64.encode(value));
         temporarySettings.put(key, newValue);
         if (!newValue.equals(oldValue)) {
             firePropertyChangeEvent(key, oldValue, newValue);
         }
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#getByteArray(java.lang.String, byte[])
      */
     public byte[] getByteArray(String key, byte[] defaultValue) {
         checkRemoved();
         String value = internalGet(key, null);
         return value == null ? defaultValue : Base64.decode(value.getBytes());
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#keys()
      */
     public String [] keys() throws BackingStoreException {
         checkRemoved();
         HashSet allKeys = new HashSet (Arrays.asList(getOriginal().keys()));
         allKeys.addAll(temporarySettings.keySet());
         return (String []) allKeys.toArray(new String [allKeys.size()]);
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#childrenNames()
      */
     public String [] childrenNames() throws BackingStoreException {
         checkRemoved();
         return getOriginal().childrenNames();
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#parent()
      */
     public Preferences parent() {
         checkRemoved();
         return manager.getWorkingCopy((IEclipsePreferences) getOriginal().parent());
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#nodeExists(java.lang.String)
      */
     public boolean nodeExists(String pathName) throws BackingStoreException {
         // short circuit for this node
 if (pathName.length() == 0) {
             return removed ? false : getOriginal().nodeExists(pathName);
         }
         return getOriginal().nodeExists(pathName);
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#name()
      */
     public String name() {
         return getOriginal().name();
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#absolutePath()
      */
     public String absolutePath() {
         return getOriginal().absolutePath();
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#flush()
      */
     public void flush() throws BackingStoreException {
         if (removed) {
             getOriginal().removeNode();
             return;
         }
         checkRemoved();
         // update underlying preferences
 for (Iterator i = temporarySettings.keySet().iterator(); i.hasNext();) {
             String key = (String ) i.next();
             String value = (String ) temporarySettings.get(key);
             if (value == null) {
                 getOriginal().remove(key);
             } else {
                 getOriginal().put(key, value);
             }
         }
         // clear our settings
 temporarySettings.clear();

         // save the underlying preference store
 getOriginal().flush();
     }

     /* (non-Javadoc)
      * @see org.osgi.service.prefs.Preferences#sync()
      */
     public void sync() throws BackingStoreException {
         checkRemoved();
         // forget our settings
 temporarySettings.clear();
         // load the underlying preference store
 getOriginal().sync();
     }

     /**
      * @return Returns the original preference node.
      */
     private IEclipsePreferences getOriginal() {
         return original;
     }
 }

